module console;

import std.boxer;
import std.conv;
import std.thread;
import std.string;
import std.stdio;
import std.stream;
import derelict.readline.readline;
import utils.color;

alias void delegate(Box[] args) CommandHandler;
alias void delegate(char[][] args) OldCommandHandler;
alias Box delegate(char[][] args, inout int argIdx) TypeHandler;


///holds one available command
struct Command {
    char[] name;
    CommandHandler handler;
    char[] helpText;
    char[][] types;
    char[][] typeHelp;
    int minArgCount;
    int id;

    public static Command opCall(char[] name, CommandHandler handler,
        char[] helpText, char[][] types, char[][] typeHelp, int minArgCount,
        int id)
    {
        assert(types.length == typeHelp.length);
        //don't ask...
        int l = types.length;
        assert(minArgCount <= l);
        Command ret;
        ret.name = name;
        ret.handler = handler;
        ret.helpText = helpText;
        ret.types = types;
        ret.typeHelp = typeHelp;
        if (minArgCount >= 0)
            ret.minArgCount = minArgCount;
        else
            ret.minArgCount = types.length;
        ret.id = id;
        return ret;
    }

    //xxx hack to support old command handlers
    OldCommandHandler oldh;

    public static Command opCall(char[] name, OldCommandHandler handler,
        char[] helpText, int id)
    {
        Command ret;
        ret.name = name;
        ret.oldh = handler;
        ret.helpText = helpText;
        ret.types = ["text"];
        ret.typeHelp = ["arg0"];
        ret.minArgCount = 0;
        ret.id = id;
        return ret;
    }

    public char[] syntaxHelp() {
        char[] res;
        foreach (int i, char[] t; types) {
            res ~= (i<minArgCount?"":"[") ~ "<" ~ typeHelp[i] ~ ": " ~ t ~ ">" ~
                (i<minArgCount?"":"]") ~ " ";
        }
        return res;
    }
}


///stores a command that is being executed from the command line
class PendingCommand {
    Command cmd;
    Box[] args;

    this(Command cmd, Box[] args) {
        this.cmd = cmd;
        this.args = args;
    }

    ///actually execute the command (main context)
    public void execute() {
        //xxx hack to support old command handlers, part 2
        if (cmd.oldh)
            if (args.length > 0)
                cmd.oldh(unbox!(char[][])(args[0]));
            else
                cmd.oldh([]);
        else
            cmd.handler(args);
    }
}


class CommandProcessor {
    private Command[] mCmds;
    private TypeHandler[char[]] mTypes;
    private int mUniqueId = 0;
    private bool mSorted = false;

    this() {
        mTypes["int"] = &parseInt;
        mTypes["float"] = &parseFloat;
        mTypes["bool"] = &parseBool;
        mTypes["string"] = &parseString;
        mTypes["text"] = &parseText;
        mTypes["color"] = &parseColor;

        registerCommand("help",&cmdHelp,"List available commands",[],[]);
    }

    // ******************** BEGIN type parsers ************************
    // those take an arguments array and an index into it and try to parse one
    // specific type
    // after execution, argIdx should point to the next unprocessed argument
    // only Exception to throw should be ConvError
    // you can assume that at least args[argIdx] is valid

    //type: int
    private Box parseInt(char[][] args, inout int argIdx) {
        Box res;
        res = box(toInt(args[argIdx]));
        argIdx++;
        return res;
    }

    //type: float
    private Box parseFloat(char[][] args, inout int argIdx) {
        Box res;
        res = box(toFloat(args[argIdx]));
        argIdx++;
        return res;
    }

    //type: bool
    private Box parseBool(char[][] args, inout int argIdx) {
        Box res;
        if (args[argIdx] == "1" || args[argIdx] == "true" || args[argIdx] == "on" || args[argIdx] == "yes")
            res = box(true);
        else if (args[argIdx] == "0" || args[argIdx] == "false" || args[argIdx] == "off" || args[argIdx] == "no")
            res = box(false);
        else
            throw new ConvError("boolean expected");
        argIdx++;
        return res;
    }

    //type: char[]
    private Box parseString(char[][] args, inout int argIdx) {
        Box res;
        res = box(args[argIdx]);
        argIdx++;
        return res;
    }

    //puts the rest of the command line into one array of strings
    //use only as last command
    //type: char[][]
    private Box parseText(char[][] args, inout int argIdx) {
        Box res;
        res = box(args[argIdx..args.length]);
        argIdx = args.length;
        return res;
    }

    //type: RGBColor
    private Box parseColor(char[][] args, inout int argIdx) {
        RGBColor out_col;
        RGBColor[char[]] colors;
        colors["white"] = RGBColor(1.0f);
        colors["black"] = RGBColor(0.0f);
        colors["off"] = RGBColor(0.0f);
        colors["grey"] = RGBColor(0.5f);
        colors["red"] = RGBColor(1.0f,0,0);
        colors["blue"] = RGBColor(0,0,1.0f);
        colors["green"] = RGBColor(0,1.0f,0);
        if (args.length-argIdx >= 3 && isNumeric(args[argIdx]) && isNumeric(args[argIdx+1])
            && isNumeric(args[argIdx+2]))
        {
            out_col.r = atof(args[argIdx]);
            out_col.g = atof(args[argIdx+1]);
            out_col.b = atof(args[argIdx+2]);
            argIdx += 3;
        } else if (args.length-argIdx >= 1 && isNumeric(args[argIdx])) {
            out_col.r = atof(args[argIdx]);
            out_col.g = atof(args[argIdx]);
            out_col.b = atof(args[argIdx]);
            argIdx++;
        } else if (args[argIdx] in colors) {
            out_col = colors[args[argIdx]];
            argIdx++;
        } else {
            writefln("unknown color... availaible:");
            foreach (char[] name, RGBColor col; colors) {
                writefln("  %s: %s", name, col);
            }
            throw new ConvError("Invalid color");
        }
        return box(out_col);
    }

    // ******************** END type parsers ************************

    //sort registered commands alphabetically
    //slow bubble sort, but command count should be low
    private void sortCommands() {
        for (int i = 0; i < mCmds.length; i++) {
            for (int j = i; j < mCmds.length; j++) {
                if (mCmds[i].name > mCmds[j].name) {
                    Command tmp;
                    tmp = mCmds[i];
                    mCmds[i] = mCmds[j];
                    mCmds[j] = tmp;
                }
            }
        }
    }

    /** add a new command
     * not sure if this is safe... better add commands before calling start()
     * Params:
     *   types = array of expected arg types (must match registered type parsers,
     *           "string" to parse yourself
     *   typeHelp = array of parameter identifiers, no < >
     *              must match length of types
     *   minArgCount = minimum number of parameters
     *                 any more arguments are optional, empty for no opt. args
     */
    public void registerCommand(char[] name, CommandHandler handler,
        char[] helpText, char[][] types, char[][] typeHelp, int minArgCount = -1)
    {
        mCmds ~= Command(name, handler, helpText, types, typeHelp, minArgCount,
            mUniqueId++);
        mSorted = false;
    }

    private OldCommandHandler[char[]] mOldHandlers;

    ///"old" command register
    ///no type checking, arbitrary number of arguments
    public void registerCommand(char[] name, OldCommandHandler handler,
        char[] helpText)
    {
        mCmds ~= Command(name, handler, helpText, mUniqueId++);
        mSorted = false;
    }

    //split command line into arguments, considering quotes
    private char[][] splitCmdLine(char[] cmdLine) {
        //return split(cmdLine);
        char[][] ret;
        char[] cur;
        bool quot = false;

        void add(inout char[] s) {
            if (s.length>0) {
                ret ~= s;
                s = "";
            }
        }

        foreach (char c; cmdLine) {
            switch (c) {
                case '"':
                    if (quot)
                        add(cur);
                    quot = !quot;
                    break;
                case ' ':
                    if (quot)
                        cur ~= c;
                    else
                        add(cur);
                    break;
                default:
                    cur ~= c;
            }
        }
        add(cur);
        return ret;
    }

    /** Parse an entered command line
     * Params:
     *   cmdLine = full command line starting with command name
     *   quiet = true to supress parse error output
     * Returns:
     *   filled PendingCommand class on success, null on failure
     */
    public PendingCommand parse(char[] cmdLine, bool quiet = false) {
        PendingCommand res = null;
        char[][] args = splitCmdLine(cmdLine);
        if (args.length>0) {
            char[] cmdName = args[0];
            if (args.length>1)
                args = args[1..args.length];
            else
                args.length = 0;
            res = call(cmdName,args);
        }
        return res;
    }

    /** Invoke a command by its name, passing args as arguments
     * Params:
     *   name = Command name
     *   args = Command arguments
     *   quiet = true to supress invalid command error
     * Returns:
     *   filled PendingCommand class on success, null on failure
     */
    public PendingCommand call(char[] name, char[][] args, bool quiet = false) {
        class ArgCountError: Exception { this(char[] msg) {super(msg);} };
        class CmdError: Exception { this(char[] msg) {super(msg);} };

        PendingCommand res;
        int cmdIdx = -1;
        foreach (int i, Command cmd; mCmds) {
            if (cmd.name == name) {
                cmdIdx = i;
            }
        }
        if (cmdIdx >= 0) {
            Command cmd = mCmds[cmdIdx];
            Box[] handArgs;

            try {
                int argIdx = 0;
                foreach (int n, char[] t; cmd.types) {
                    try {
                        if (argIdx >= args.length && argIdx < cmd.minArgCount) {
                            throw new ArgCountError("Not enough arguments");
                        }
                        if (argIdx < args.length) {
                            if (t in mTypes) {
                                handArgs ~= mTypes[t](args,argIdx);
                            } else {
                                assert(false,"Invalid type");
                            }
                        }
                    } catch (ConvError e) {
                        writefln("Error: Expecting %s for %d. argument <%s>",t,n+1,cmd.typeHelp[n]);
                        throw new CmdError("err");
                    } catch (ArgCountError e) {
                        if (cmd.minArgCount != cmd.types.length)
                            writefln("Error: Expecting %d to %d arguments",cmd.minArgCount,cmd.types.length);
                        else
                            writefln("Error: Expecting %d argument%s",cmd.minArgCount,cmd.minArgCount==1?"":"s");
                        throw new CmdError("err");
                    }
                }
                res = new PendingCommand(cmd, handArgs);
            } catch(CmdError e) {
                writefln("Syntax: %s %s",name,cmd.syntaxHelp);
            }

        } else {
            if (!quiet)
                writefln("Invalid command: %s",name);
        }
        return res;
    }

    ///Parse cmdLine and execute directly
    public void parseAndExecute(char[] cmdLine, bool quiet = false) {
        PendingCommand pendCmd = parse(cmdLine,quiet);
        if (pendCmd)
            pendCmd.execute();
    }

    public void parseAndExecute(char[] name, char[][] args, bool quiet = false) {
        PendingCommand pendCmd = call(name,args,quiet);
        if (pendCmd)
            pendCmd.execute();
    }

    ///Execute a script file directly
    public void executeScript(File scrFile) {
        foreach (char[] line; scrFile) {
            parseAndExecute(line);
        }
    }

    public void executeScript(char[][] script) {
        foreach (char[] line; script) {
            parseAndExecute(line);
        }
    }

    private void cmdHelp(Box[] args) {
        if (!mSorted)
            sortCommands();
        writefln("Available commands:");
        foreach (Command cmd; mCmds) {
            writefln("%s: %s",cmd.name,cmd.helpText);
        }
    }
}


///simple, array-based queue, not very fast
///pop() is O(n), push() is probably O(n)
class Queue(T) {
    private T[] queue;

    this() {
        queue = new T[0];
    }

    public void push(T element) {
        queue ~= element;
    }

    public T pop() {
        if (queue.length>0) {
            T ret = queue[0];
            for (int i = 0; i < queue.length-1; i++)
                queue[i] = queue[i+1];
            queue.length = queue.length-1;
            return ret;
        } else {
            throw new Exception("Queue empty");
        }
    }

    public int size() {
        return queue.length;
    }
}

alias Queue!(PendingCommand) PendCmdQueue;


class Console: Thread {
    private CommandProcessor mCmdProc;
    private PendCmdQueue mPendQueue;
    private bool mTerminated;

    this(CommandProcessor cmdProc) {
        super();
        DerelictReadline.load();
        using_history();
        mCmdProc = cmdProc;
        mPendQueue = new PendCmdQueue();
    }

    ~this() {
        DerelictReadline.unload();
    }

    // --------- BEGIN DETACHED CODE -------------

    protected int run() {
        //seems to terminate magically when the program ends...
        do {
            char[] cmd = std.string.toString(readline("> "));
            if (cmd.length > 0)
                add_history(toStringz(cmd));
            PendingCommand pendCmd = mCmdProc.parse(cmd);
            if (pendCmd)
                prepareAndWait(pendCmd);
            yield();
        } while (!mTerminated);
        return 0;
    }

    ///add a command parsed from an entered command line to the execution queue
    private void prepareAndWait(PendingCommand pendCmd) {
        synchronized(mPendQueue) {
            mPendQueue.push(pendCmd);
        }
        //wait for command execution
        while(true) {
            synchronized(mPendQueue) {
                if (mPendQueue.size == 0)
                    break;
            }
            yield();
        }
    }

    // --------- END DETACHED CODE -------------

    public void stop() {
        //xxx ich kriegs ned hin... :(
        //rl_variable_bind("rl_done","1");
        mTerminated = true;
    }

    ///call periodically to execute pending commands
    ///one call will execute 0 or 1 command in the context of the caller
    public void checkCommands() {
        synchronized(mPendQueue) {
            if (mPendQueue.size > 0) {
                PendingCommand cmd = mPendQueue.pop();
                cmd.execute();
            }
        }
    }

    public bool commandsPending() {
        return (mPendQueue.size > 0);
    }
}

